package com.train.daily.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.train.common.constants.KeyConstants;
import com.train.common.enums.impl.BizExceptionEnums;
import com.train.common.enums.impl.SysExceptionEnums;
import com.train.common.exception.GlobalException;
import com.train.common.resp.CommonResp;
import com.train.common.resp.PageResp;
import com.train.daily.dto.seat.DailyTrainSeatCountDTO;
import com.train.daily.dto.ticket.DailyTrainTicketPageDTO;
import com.train.daily.dto.ticket.DailyTrainTicketQueryOneDTO;
import com.train.daily.dto.ticket.DailyTrainTicketReduceTicketDTO;
import com.train.daily.mapper.DailyTrainTicketMapper;
import com.train.daily.po.DailyTrainTicketPO;
import com.train.daily.resp.ticket.DailyTrainTicketPageResp;
import com.train.daily.resp.ticket.DailyTrainTicketQueryOneResp;
import com.train.daily.service.DailyTrainSeatService;
import com.train.daily.service.DailyTrainTicketService;
import com.train.openfeign.core.utils.OpenFeignUtils;
import com.train.redis.core.support.RedisSupport;
import com.train.redis.core.support.RedissonSupport;
import com.train.redis.core.utils.KeyUtils;
import com.train.redis.core.utils.SerializableUtils;
import com.train.system.api.TrainApi;
import com.train.system.api.TrainStationApi;
import com.train.system.enums.impl.SeatTypeEnums;
import com.train.system.resp.train.TrainListResp;
import com.train.system.resp.trainstation.TrainStationListResp;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author baitiaojun
 * @description 针对表【dailyTrainTicket(余票信息)】的数据库操作Service实现
 * @createDate 2025-03-07 00:09:31
 */
@Service
public class DailyTrainTicketServiceImpl extends ServiceImpl<DailyTrainTicketMapper, DailyTrainTicketPO>
    implements DailyTrainTicketService {

    private final DailyTrainTicketMapper dailyTrainTicketMapper;

    private final TrainStationApi trainStationApi;

    private final TrainApi trainApi;

    private final DailyTrainSeatService dailyTrainSeatService;

    private final RedissonSupport redissonSupport;

    private final RedisSupport redisSupport;

    public DailyTrainTicketServiceImpl(DailyTrainTicketMapper dailyTrainTicketMapper,
                                       @Qualifier("com.train.system.api.TrainStationApi") TrainStationApi trainStationApi,
                                       @Qualifier("com.train.system.api.TrainApi") TrainApi trainApi,
                                       DailyTrainSeatService dailyTrainSeatService, RedissonSupport redissonSupport, RedisSupport redisSupport) {
        this.dailyTrainTicketMapper = dailyTrainTicketMapper;
        this.trainStationApi = trainStationApi;
        this.trainApi = trainApi;
        this.dailyTrainSeatService = dailyTrainSeatService;
        this.redissonSupport = redissonSupport;
        this.redisSupport = redisSupport;
    }

    /**
     * 添加分布式锁+快速失败防止缓存击穿, 限流+缓存空对象防止缓存穿透
     * @param dailyTrainTicketPageDTO
     * @return
     */
    @Override
    public PageResp<DailyTrainTicketPageResp> pageList(DailyTrainTicketPageDTO dailyTrainTicketPageDTO) {
        String key = KeyUtils.join(KeyConstants.DAILY_TRAIN_TICKET_DATE_TRAIN_KEY_PREFIX, dailyTrainTicketPageDTO);
        //缓存有数据直接返回
        PageResp<DailyTrainTicketPageResp> pageResp = getDailyTrainTicket(key);
        if (pageResp != null) {
            return pageResp;
        }

        //缓存没有数据到数据库查询, 先获取分布式锁, 只允许一个线程去数据库查
        //加锁的时候只针对指定日期下指定车次查询加锁, 不要阻塞其他的日期和车次查询
        String ticketLockKey = KeyUtils.join(KeyConstants.DAILY_TRAIN_TICKET_LOCK_KEY_PREFIX, dailyTrainTicketPageDTO);
        RLock lock = redissonSupport.getLock(ticketLockKey);
        try {
            //多余的线程等待2秒后快速失败
            if (!lock.tryLock(2L, TimeUnit.SECONDS)) {
                throw new GlobalException(BizExceptionEnums.EXP_PAGE_DAILY_TRAIN_TICKET);
            }

            //等待的线程如果能从缓存获取
            pageResp = getDailyTrainTicket(key);
            if (pageResp != null) {
                return pageResp;
            }

            //添加读锁, 支持并行读, 但是在写数据的时候不允许读, 防止读的时候脏数据更到缓存
            String readWriteLockKey = KeyUtils.join(KeyConstants.DAILY_TRAIN_TICKET_READ_WRITE_LOCK_KEY_PREFIX, dailyTrainTicketPageDTO);
            RReadWriteLock readWriteLock = redissonSupport.getReadWriteLock(readWriteLockKey);
            RLock readLock = readWriteLock.readLock();
            try {
                //等待2秒, 如果写线程还没写完就直接失败
                if (!readLock.tryLock(2L, TimeUnit.SECONDS)) {
                    throw new GlobalException(BizExceptionEnums.EXP_PAGE_DAILY_TRAIN_TICKET);
                }
                pageResp = dailyTrainTicketMapper.pageList(dailyTrainTicketPageDTO);
                //无论查没查到新数据都放到缓存, 热点数据保存14天, 防止冷数据占用缓存
                redisSupport.addValue(key, pageResp, 14L, TimeUnit.DAYS);
                return pageResp;
            } finally {
                redissonSupport.unlock(readLock);
            }
        } catch (InterruptedException e) {
            throw new GlobalException(SysExceptionEnums.EXP_SERVER);
        } finally {
            redissonSupport.unlock(lock);
        }

    }

    private PageResp<DailyTrainTicketPageResp> getDailyTrainTicket(String key) {
        Object value = redisSupport.getValue(key);
        if (value == null) {
            return null;
        }
        PageResp<?> pageResp = SerializableUtils.parseObject(value, PageResp.class);
        if (pageResp == null) {
            throw new GlobalException(SysExceptionEnums.EXP_SERVER);
        }
        //空缓存直接返回, 防止缓存穿透
        List<?> data = pageResp.getData();
        if (CollectionUtil.isEmpty(data)) {
            return new PageResp<>(0L, ListUtil.empty());
        }
        //不是空数据转换类型返回
        List<DailyTrainTicketPageResp> ticketPageRespList = BeanUtil.copyToList(data, DailyTrainTicketPageResp.class);
        return new PageResp<>(pageResp.getTotal(), ticketPageRespList);

    }


    @Override
    public void remove(Long id) {
        boolean isRemoved = this.removeById(id);
        if (!isRemoved) {
            throw new GlobalException(BizExceptionEnums.EXP_REMOVE_DAILY_TRAIN_TICKET);
        }
    }

    @Override
    public void generateDailyTrainTicket(String date, List<String> trainCodes) {
        //删除当天车次车票
        if (dailyTrainTicketMapper.existsTrains(LocalDate.parse(date), trainCodes)) {
            boolean isDeleted = dailyTrainTicketMapper.deleteTrains(LocalDate.parse(date), trainCodes);
            if (!isDeleted) {
                throw new GlobalException(BizExceptionEnums.EXP_ADD_DAILY_TRAIN_TICKET);
            }
        }

        //查询每日车次
        CommonResp trainListResp = trainApi.getTrainListByTrainCodes(trainCodes);
        List<TrainListResp> trainList = OpenFeignUtils.getResultList(trainListResp, TrainListResp.class);
        if (CollectionUtil.isEmpty(trainList)) {
            throw new GlobalException(BizExceptionEnums.EXP_TRAIN_NOT_EXISTS);
        }

        //查询该日所有途径站
        CommonResp resp = trainStationApi.getTrainStationListByTrainCodes(trainCodes);

        //转换类型
        List<TrainStationListResp> trainStationListResp = OpenFeignUtils.getResultList(resp, TrainStationListResp.class);
        if (CollectionUtil.isEmpty(trainStationListResp)) {
            throw new GlobalException(BizExceptionEnums.EXP_TRAIN_STATION_NOT_EXISTS);
        }

        TrainStationListResp start;
        TrainStationListResp end;

        //添加始发站和终点站
        for (TrainListResp train : trainList) {
            start = new TrainStationListResp();
            end = new TrainStationListResp();
            start.setIndex(0);
            start.setTrainCode(train.getCode());
            start.setName(train.getStart());
            start.setNamePinyin(train.getStartPinyin());
            start.setInTime(train.getStartTime());
            end.setIndex(trainStationListResp.size() + 1);
            end.setTrainCode(train.getCode());
            end.setName(train.getEnd());
            end.setNamePinyin(train.getEndPinyin());
            end.setOutTime(train.getEndTime());
            trainStationListResp.addAll(ListUtil.of(start, end));
        }

        //trainStationList排序,转为code -> list
        Map<String, List<TrainStationListResp>> trainStationMap = trainStationListResp.stream()
                .sorted(Comparator.comparingInt(TrainStationListResp::getIndex))
                .collect(Collectors.groupingBy(TrainStationListResp::getTrainCode));

        //查询每日车厢座位数量
        Map<String, DailyTrainSeatCountDTO> dailyTrainSeatBOMap = dailyTrainSeatService.countDailyTrainSeat(LocalDate.parse(date), trainCodes);

        //重新添加当天车次车票
        List<DailyTrainTicketPO> dailyTrainTicketPOS = new ArrayList<>();
        DailyTrainTicketPO dailyTrainTicketPO;
        Set<String> trainCodesList = trainStationMap.keySet();
        for (String trainCode : trainCodesList) {
            List<TrainStationListResp> trainStationList = trainStationMap.get(trainCode);
            for (int i = 0; i < trainStationList.size(); i++) {
                TrainStationListResp trainStationStart = trainStationList.get(i);
                for (int j = i + 1; j < trainStationList.size(); j++) {
                    TrainStationListResp trainStationEnd = trainStationList.get(j);
                    dailyTrainTicketPO = new DailyTrainTicketPO();
                    dailyTrainTicketPO.setDate(LocalDate.parse(date));
                    dailyTrainTicketPO.setTrainCode(trainCode);
                    dailyTrainTicketPO.setStart(trainStationStart.getName());
                    dailyTrainTicketPO.setStartPinyin(trainStationStart.getNamePinyin());
                    dailyTrainTicketPO.setStartTime(trainStationStart.getInTime());
                    dailyTrainTicketPO.setStartIndex(trainStationStart.getIndex());
                    dailyTrainTicketPO.setEnd(trainStationEnd.getName());
                    dailyTrainTicketPO.setEndPinyin(trainStationEnd.getNamePinyin());
                    dailyTrainTicketPO.setEndTime(trainStationEnd.getOutTime());
                    dailyTrainTicketPO.setEndIndex(trainStationEnd.getIndex());
                    DailyTrainSeatCountDTO ydz = dailyTrainSeatBOMap.get(trainCode + SeatTypeEnums.YDZ.getCode());
                    dailyTrainTicketPO.setYdz(ydz != null ? Math.toIntExact(ydz.getCount()) : 0);
                    dailyTrainTicketPO.setYdzPrice(new BigDecimal(0));
                    DailyTrainSeatCountDTO edz = dailyTrainSeatBOMap.get(trainCode + SeatTypeEnums.EDZ.getCode());
                    dailyTrainTicketPO.setEdz(edz != null ? Math.toIntExact(edz.getCount()) : 0);
                    dailyTrainTicketPO.setEdzPrice(new BigDecimal(0));
                    DailyTrainSeatCountDTO rw = dailyTrainSeatBOMap.get(trainCode + SeatTypeEnums.RW.getCode());
                    dailyTrainTicketPO.setRw(rw != null ? Math.toIntExact(rw.getCount()) : 0);
                    dailyTrainTicketPO.setRwPrice(new BigDecimal(0));
                    DailyTrainSeatCountDTO yw = dailyTrainSeatBOMap.get(trainCode + SeatTypeEnums.YW.getCode());
                    dailyTrainTicketPO.setYw(yw != null ? Math.toIntExact(yw.getCount()) : 0);
                    dailyTrainTicketPO.setYwPrice(new BigDecimal(0));
                    dailyTrainTicketPOS.add(dailyTrainTicketPO);
                }
            }
        }

        Map<String, List<DailyTrainTicketPO>> dailyTrainTicketMap = dailyTrainTicketPOS.stream()
                .collect(Collectors.groupingBy(DailyTrainTicketPO::getTrainCode));
        for (String trainCode : trainCodes) {
            //获取写锁进行数据库更新和redis更新, 读的时候不允许写, 写的时候不允许读
            RReadWriteLock readWriteLock = redissonSupport.getReadWriteLock(KeyConstants.DAILY_TRAIN_TICKET_READ_WRITE_LOCK_KEY_PREFIX, date, trainCode);
            RLock writeLock = readWriteLock.writeLock();
            try {
                //写失败就快速失败抛出异常回滚事物
                if (!writeLock.tryLock(0L, TimeUnit.SECONDS)) {
                    throw new GlobalException(BizExceptionEnums.EXP_NOT_ALLOW_ADD_DAILY_TRAIN_TICKET, date, trainCode);
                }

                boolean isSaved = this.saveBatch(dailyTrainTicketMap.get(trainCode));
                if (!isSaved) {
                    throw new GlobalException(BizExceptionEnums.EXP_ADD_DAILY_TRAIN);
                }

                //防止重复删除空数据
                if (redisSupport.exists(KeyConstants.DAILY_TRAIN_TICKET_DATE_TRAIN_KEY_PREFIX + "*")) {
                    redisSupport.delete(KeyConstants.DAILY_TRAIN_TICKET_DATE_TRAIN_KEY_PREFIX + "*");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                redissonSupport.unlock(writeLock);
            }
        }
    }

    @Override
    public DailyTrainTicketQueryOneResp getDailyTrainTicket(DailyTrainTicketQueryOneDTO dto) {
        return dailyTrainTicketMapper.getDailyTrainTicket(dto);
    }

    /**
     * 批量减少余票
     * @param dtoList
     */
    @Override
    public void batchReduceDailyTrainTicket(List<DailyTrainTicketReduceTicketDTO> dtoList) {
        for (DailyTrainTicketReduceTicketDTO dto : dtoList) {
            //计算车站车票库存扣减区间
            //A B C D E F G H I J
            // 1 0 1 1 1 0 0 0 1
            Integer startIndex = dto.getStartIndex();
            Integer endIndex = dto.getEndIndex();
            int maxStart = endIndex - 1; //车站最大开始序号
            int minEnd = startIndex + 1; //车站最小结束序号
            char[] sellArray = dto.getSell().toCharArray(); //转数组

            //车站最小开始序号
            int minStart = 0;
            //需要startIndex - 1, 车站是10个, 途径车站的序列是9个, 所以需要用车站下标-1才能正确取到车站间的售票值
            for (int i = startIndex - 1; i >= 0; i--) {
                char sell = sellArray[i];
                if (sell == '1') {
                    //找到sell中为1的值, 从它的终点站开始有影响, 终点站作为车站最小开始序号
                    minStart = i + 1;
                    break;
                }
            }

            //车站最大结束序号
            int maxEnd = dto.getSell().length();
            //dto.getSell().length()的值和sell售票列表中的下标对应, 所以直接获取下标对应的sell值即可
            for (int i = endIndex; i < dto.getSell().length(); i++) {
                char sell = sellArray[i];
                if (sell == '1') {
                    maxEnd = i;
                    break;
                }
            }

            //更新库存
            boolean isEdit = dailyTrainTicketMapper
                    .reduceDailyTrainTicket(dto.getTrainCode(), dto.getDate(), dto.getSeatType(), minStart, maxStart, minEnd, maxEnd);
            if (!isEdit) {
                throw new GlobalException(BizExceptionEnums.EXP_REDUCE_DAILY_TRAIN_TICKET);
            }
        }

    }
}




